﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Net;

namespace FileSharing
{
    public class Connection
    {
        public Vizinho ConnectVizinho { get; set; }
        public IPEndPoint ConnectTo;

        public IPEndPoint RemoteHost
        {
            get { return (IPEndPoint)socket.Client.RemoteEndPoint; }
        }

        public IPEndPoint LocalHost
        {
            get { return (IPEndPoint)socket.Client.LocalEndPoint; }
        }

        TcpClient socket;

        Thread ListenThread;

        ProtocolHandler Protocol;

        Mutex MutSend = new Mutex();

        bool IsServer;

        public Connection(ProtocolHandler prot)
        {
            Protocol = prot;
        }

        public void Connect(IPEndPoint ip)
        {
            ConnectTo = ip;

            socket = null;
            IsServer = false;

            ListenThread = new Thread(new ThreadStart(Listen));
            ListenThread.Start();
        }

        public void Start(TcpClient client)
        {
            ConnectTo = (IPEndPoint)client.Client.RemoteEndPoint;

            socket = client;
            IsServer = true;

            ListenThread = new Thread(new ThreadStart(Listen));
            ListenThread.Start();
        }

        private void Listen()
        {
            try
            {
                NetworkStream stream;
                BinaryReader reader;

                ConnectVizinho = new Vizinho();

                if (socket == null)
                {
                    socket = new TcpClient();
                    socket.Connect(ConnectTo);

                    ConnectVizinho.IP = ConnectTo;
                }

                ConnectVizinho.CurrentStatus = Vizinho.Status.Connected;

                Log.Instance().PrintLine("Connected to " + ConnectTo.ToString());

                if (!IsServer)
                {
                    SendMessage(new MessageHello(Protocol.LocalAddress.Port, RemoteHost.Address, Protocol.LocalName));
                }

                stream = socket.GetStream();
                reader = new BinaryReader(stream);

                while (true)
                {
                    try
                    {
                        // Decode the message
                        Message mens = Message.CreateMessage(reader);

                        if (mens.GetType() == typeof(MessageTransferHello))
                        {
                            MessageTransferHello mensHello = (MessageTransferHello)mens;
                            TransferConnection trans = new TransferConnection();
                            UploadTask up = new UploadTask(trans, Protocol.UploadsCollection);

                            trans.StartUpload(socket, Protocol.GetVizinho(mensHello.Host), up);

                            Protocol.VizinhoRejected(this);
                            Protocol.NewUploadTask(up);

                            break;
                        }

                        if (mens.Processed(Protocol) == false)
                        {
                            // Process the message action
                            mens.Process(Protocol, this);

                            //Log.Instance().PrintLine("Received " + mens.ToString() + " from " + ConnectVizinho.IP.ToString());

                            // Answer
                            Message answer = mens.Answer(Protocol, this);

                            // Send the answare to sender
                            SendMessage(answer);
                        }
                    }
                    catch (NoAnswerException e)
                    {
                    }
                    catch (IOException e)
                    {
                        throw e;
                    }
                }
            }
            catch (UnknownMessageException e)
            {
                Protocol.VizinhoRejected(this);
                Protocol.VizinhoDead(this);
            }
            catch (IOException e)
            {
                Protocol.VizinhoDead(this);
            }
            catch (SocketException e)
            {
                if (e.ErrorCode == 10061) //Connection refused
                {
                    Log.Instance().PrintLine("Connection refused to " + ConnectTo.ToString());
                }

                Protocol.VizinhoRejected(this);
            }
        }

        public void SendMessage(Message mens)
        {
            MutSend.WaitOne();

            //Log.Instance().PrintLine("Sending " + mens.ToString() + " to " + ConnectVizinho.IP.ToString());

            try
            {
                NetworkStream stream = socket.GetStream();
                BinaryWriter writer = new BinaryWriter(stream);
                mens.Serialize(writer);
            }
            catch (ObjectDisposedException e)
            {
            }
            catch (InvalidOperationException e)
            {
            }

            MutSend.ReleaseMutex();
        }

        public void Close()
        {
            if (socket != null)
                socket.Close();
        }
    }

    public class TransferConnection
    {
        TcpClient socket;
        BinaryWriter writer;
        Thread TaskThread;
        Thread SendPieceThread;
        Thread TrafficControlThread;

        DownloadTask Download;
        UploadTask Upload;

        private const int TrafficSamples = 30;
        private const int TrafficSamplingPeriod = 70; // ms
        int[] DownloadTraffic = new int[TrafficSamples];
        int LastTraffic;

        Mutex MutSend = new Mutex();
        Mutex MutTraffic = new Mutex();

        Piece TemporaryPiece;

        bool IsServer;
        bool IsDownload;

        bool IsRunning = true;

        IPEndPoint LocalAddress;

        public Vizinho ConnectVizinho { get; set; }

        public IPEndPoint RemoteHost
        {
            get { return (IPEndPoint)socket.Client.RemoteEndPoint; }
        }

        public IPEndPoint LocalHost
        {
            get { return (IPEndPoint)socket.Client.LocalEndPoint; }
        }

        public UploadTask UploadTask { get { return Upload; } }
        public DownloadTask DownloadTask { get { return Download; } }

        public byte[] AvaiblePieces { get; set; }

        public void StartDownload(Vizinho viz, IPEndPoint localAddress, DownloadTask task)
        {
            Download = task;
            IsDownload = true;

            ConnectVizinho = viz;
            LocalAddress = localAddress;

            socket = null;
            IsServer = false;

            TaskThread = new Thread(new ThreadStart(Listen));
            TaskThread.Start();

            TrafficControlThread = new Thread(new ThreadStart(TrafficControl));
            TrafficControlThread.Start();
        }

        public void StartUpload(TcpClient client, Vizinho viz, UploadTask task)
        {
            Upload = task;
            IsDownload = false;

            ConnectVizinho = viz;

            socket = client;
            IsServer = true;

            TaskThread = new Thread(new ThreadStart(Listen));
            TaskThread.Start();

            TrafficControlThread = new Thread(new ThreadStart(TrafficControl));
            TrafficControlThread.Start();
        }

        private void Listen()
        {
            try
            {
                BinaryReader reader;

                if (socket == null)
                {
                    socket = new TcpClient();
                    socket.Connect(ConnectVizinho.IP);
                }

                writer = new BinaryWriter(socket.GetStream());
                reader = new BinaryReader(socket.GetStream());

                if (!IsServer)
                {
                    SendHello();
                }
                else
                {
                    SendMessage(new MessageTransferHelloReply());
                }

                IsRunning = true;

                while (true)
                {
                    try
                    {
                        // Decode the message
                        TransferMessage mens = TransferMessage.CreateMessage(reader);

                        // Process the message action
                        mens.Process(this);

                        if (mens.GetType() == typeof(MessagePieceChunk))
                        {
                            MutTraffic.WaitOne();
                            LastTraffic += LocalFile.ChunkSize;
                            MutTraffic.ReleaseMutex();
                        }

                        // Answer
                        TransferMessage answer = mens.Answer(this);

                        // Send the answare to sender
                        SendMessage(answer);
                    }
                    catch (NoAnswerException e)
                    {
                    }
                    catch (IOException e)
                    {
                        throw e;
                    }
                }
            }
            catch (IOException e)
            {
                if (IsDownload)
                    DownloadTask.TransferEnded(this);
                else
                    UploadTask.TransferEnded();
            }
            catch (SocketException e)
            {
                // TODO: Cliend rejected connection. Send a message to user.
            }
            catch (ObjectDisposedException e)
            {
            }
            finally
            {
                IsRunning = false;
            }
        }

        public void TrafficControl()
        {
            while (IsRunning)
            {
                Thread.Sleep(TrafficSamplingPeriod);

                MutTraffic.WaitOne();

                for (int i = 0; i < TrafficSamples - 1; i++)
                {
                    DownloadTraffic[i] = DownloadTraffic[i + 1];
                }

                DownloadTraffic[TrafficSamples - 1] = LastTraffic;

                LastTraffic = 0;

                MutTraffic.ReleaseMutex();
            }
        }

        public int GetSpeed()
        {
            int result = 0;

            MutTraffic.WaitOne();

            for (int i = 0; i < TrafficSamples; i++)
            {
                result += DownloadTraffic[i];
            }

            MutTraffic.ReleaseMutex();

            return (result / TrafficSamples) * (1000 / TrafficSamplingPeriod);
        }

        public void SendMessage(TransferMessage mens)
        {
            MutSend.WaitOne();

            //if (mens.GetType() != typeof(MessagePieceChunk))
            //    Log.Instance().PrintLine("[Transfer] Sending " + mens.GetType().Name + " to " + RemoteHost.ToString());

            int bytes = mens.Serialize(writer);

            MutSend.ReleaseMutex();
        }

        private void SendHello()
        {
            MessageTransferHello mens = new MessageTransferHello(LocalAddress);

            MutSend.WaitOne();

            //Log.Instance().PrintLine("[Transfer] Sending " + mens.GetType().Name + " to " + ConnectVizinho.IP.ToString());

            mens.Serialize(writer);

            MutSend.ReleaseMutex();
        }

        public void Close()
        {
            if (socket != null)
                socket.Close();
        }

        public void TransferEnd()
        {
            if (IsDownload)
                DownloadTask.TransferEnded(this);
            else
                UploadTask.TransferEnded();

            Close();
        }

        public void StartSendPiece(int sequenceNumber)
        {
            SendPieceThread = new Thread(new ParameterizedThreadStart(SendPiece));
            SendPieceThread.Start(sequenceNumber);
        }

        private void SendPiece(object sequenceNumber)
        {
            int sequence = (int)sequenceNumber;

            try
            {
                Piece p = UploadTask.SourceFile.GetPiece(sequence);
                int ChunkCount = p.ChunkCount();

                for (int i = 0; i < ChunkCount; i++)
                {
                    byte[] chunk = p.GetChunk(i);

                    SendMessage(new MessagePieceChunk(i, chunk));

                    MutTraffic.WaitOne();
                    LastTraffic += LocalFile.ChunkSize;
                    MutTraffic.ReleaseMutex();
                }

                SendMessage(new MessagePieceEnd(p.GetHash()));
            }
            catch (IOException e)
            {
                Log.Instance().PrintLine("[Transfer] Exception " + e.ToString() + " when sending piece");
            }
            catch (Exception e)
            {
                Log.Instance().PrintLine("[Transfer] Exception " + e.ToString() + " when sending piece");
            }
        }

        public void StartReceivePiece(int sequenceNumber)
        {
            TemporaryPiece = new Piece(DownloadTask.PieceSize(sequenceNumber), sequenceNumber);
        }

        public void ReceivedChunk(int sequenceNumber, byte[] data)
        {
            TemporaryPiece.WriteChunk(data, sequenceNumber);
        }

        public void ReceivedPiece(byte[] hash)
        {
            if (TemporaryPiece.CheckHash(hash) == true)
            {
                DownloadTask.DownloadSucceeded(TemporaryPiece);
            }
            else
            {
                DownloadTask.DownloadFailed(TemporaryPiece.SequenceNumber);
            }

            int sequence;

            if (DownloadTask.NextPiece(AvaiblePieces, out sequence) == true)
            {
                StartReceivePiece(sequence);
                SendMessage(new MessageRequestPiece(sequence));
            }
            else
                SendMessage(new MessageTransferEnd(0));
        }
    }
}
